#
# fusion.core.rights
#
# Copyright 2008 Helsinki Institute for Information Technology
# and the authors.
#
# Authors: Ken Rimey <rimey@hiit.fi>
#

# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""
ETRS Authorization Scheme

Here we continue the story begun in item.py by defining the rules for
when a public key is authorized to sign a particular item.

First of all, we define a distinction between master datasets and
content datasets.  A master dataset represents a person/persona or a
group.  Every content dataset is associated with some master dataset.
A content dataset always includes the information ID of the associated
master dataset in its root item as the value of "parent".  A master
dataset and its content datasets collectively comprise a _domain_.
The owner of the master dataset is considered the owner of the domain,
and the information ID of the master dataset serves as the ID of the
domain.

Public keys identified by member items in the master dataset are
defined to be _members_ of the domain.  The owner of the domain may or
may not also be a member.

Members can be assigned named roles by including a list of these roles
as the value of "roles" in the member item.  Roles with special
meanings include "admin" (administrator) and "author".

The authorization rules are as follows.

                        Root             Key-value         Cancel

  Master dataset    domain owner       domain owner     domain owner
                                     or as authorized
                                      by rights item

  Content dataset   domain owner,     dataset owner     dataset owner,
                   author or admin   or as authorized   domain owner,
                                      by rights item      or admin

- The owner of a dataset can sign any non-root item in that dataset.

- The root item of a master dataset can be signed by any public key,
which thus becomes the owner of the domain.

- An author can create content datasets by signing root items.

- An administrator or the owner of a domain can create and delete
content datasets by signing root and cancel items.

- If a dataset contains a rights item with a key of the form
["rights", "member-write", ...], the third and following elements of
the key comprise a pattern, and members of the domain are authorized
to sign any kv item with a key matching the pattern.  That is, members
can add, update, and delete those keys.  The value of the rights item
is interpreted as a JSON object.  If this includes a list of roles as
the value of "roles", the authorization only applies to members with
one of those roles.  (If the value is invalid JSON or does not
represent a dictionary, its interpretation is undefined.  This
implementation treats it identically to "{}".)

- If a dataset contains a rights item with a key of the form
["rights", "visitor-write", ...], the third and following elements of
the key comprise a pattern, and any public key at all can add, update,
and delete keys in this dataset matching that pattern.  The new kv
item must include a copy of the public key with which it has been
signed.

A pattern is matched against a key element-by-element.  "*" matches
any string.  "!" matches the hexadecimal fingerprint of the public key
with which the kv item is signed.  "..." matches all the remaining
elements of the key, of which there must be at least one.  Any other
string matches only an identical string in the key.

By convention, a domain with one or more administrators will typically
include a rights item in the master dataset authorizing administrators
to write to any key.  This ensures that administrators have the same
rights as the owner of the domain (except to cancel the domain).
Note, however, that neither an administrator nor the owner is
automatically authorized to write to a content dataset owned by
somebody else.

An additional kind of rights item is used to control who can subscribe
to a content dataset, as enforced by the synchronization protocol.  A
content dataset is public if it contains a key of the form ["rights",
"public"].  Otherwise it is private.  Master datasets are always
public.  Anybody at all can subscribe to a public dataset, while
private datasets are restricted to domain members and the domain
owner.
"""

import simplejson

from fusion.core.item import hex_to_buffer
from fusion.db.path import path_to_list
from fusion.db.exceptions import Unauthorized

def verify(db, item, check_subscription=True):
    """
    Check that the item's signature is authorized and correct.

    The verification must be included in the transaction that inserts
    the item into the database.
    """
    try:
        return _verify(db, item, check_subscription)
    except Exception:
        return False

def _verify(db, item, check_subscription=True):
    if item.is_root() and check_subscription:
        if db.get_info(item.iid) is None:
            raise Unauthorized('No subscription.')

    root = get_root(db, item)
    if not root:
        raise Unauthorized('No root.')

    if not item.is_root() and item.verify_signature(root):
        return True             # Dataset owner can sign kv and cancel items.

    master = get_master(db, root)
    if not master:
        raise Unauthorized('No master.')

    if not item.key and item.verify_signature(master):
        return True             # Domain owner can sign root and cancel items.

    iid = root.iid
    domain = master.iid
    if iid == domain and not item.key:
        raise Unauthorized('Unable to verify non-kv item in master.')

    # Members may be able to sign kv items, and they may be able to
    # create and cancel content datasets.
    fp = item.get_signature().fingerprint
    member_item = db.get_item(domain, ['member', fp])
    if member_item is not None and member_item.value is not None:
        member_info = parse_json_object(member_item.value)
        roles = member_info.get('roles', [])
        if (isinstance(roles, list)
            and ((not item.key and 'admin' in roles)
                 or (item.is_root() and 'author' in roles)
                 or check_rights(db, item, 'member-write', roles))):
            return item.verify_signature(member_item)

    # Visitors may be able to sign kv items.
    if item.public_key and check_rights(db, item, 'visitor-write'):
        return item.verify_signature(item) # False if not self-signed.

    raise Unauthorized('Unable to verify item.')

def get_root(db, item):
    if item.is_root():
        return item
    else:
        return db.get_item_by_hash(item.iid)

def get_master(db, root):
    root_info = parse_json_object(root.value)
    if 'parent' not in root_info:
        return root

    domain = root_info['parent']
    try:
        domain = hex_to_buffer(domain)
    except TypeError:
        return None

    master = db.get_item_by_hash(domain)
    if master is None or not master.is_root():
        return None

    master_info = parse_json_object(master.value)
    if 'parent' in master_info:
        return None

    return master

def check_rights(db, item, rights_type, roles=None):
    if item.key:
        iid = item.iid
        for pattern in db.keys(iid, ['rights', rights_type]):
            pattern = path_to_list(pattern)
            if match_item(pattern, item):
                rights_info = db.get_rights(iid, rights_type, pattern)
                if roles is None \
                        or 'roles' not in rights_info \
                        or any(x in roles for x in rights_info['roles']):
                    return True

    return False

def parse_json_object(s):
    """
    Decode a JSON object.

    An empty dictionary is returned if the argument string is invalid
    JSON or does not encode a dictionary.

    >>> parse_json_object('{"answer":42}')
    {u'answer': 42}
    >>> parse_json_object('blah')
    {}
    >>> parse_json_object('42')
    {}
    >>> parse_json_object('')
    {}
    """
    try:
        v = simplejson.loads(s)
        return v if isinstance(v, dict) else {}
    except ValueError:
        return {}

def match_item(pattern, item):
    """
    Test if the pattern matches the item.

    >>> from fusion.core.identity import generate
    >>> identity = generate()
    >>> fp = identity.fingerprint
    >>> from fusion.core.item import create_root_item, create_kv_item
    >>> root = create_root_item(identity)
    >>> item = create_kv_item(root.iid, ['foo', fp], 'whatever', 1, identity)
    >>> match_item(['foo', '!'], item)
    True
    >>> match_item(['foo', '*'], item)
    True
    >>> match_item(['!', '*'], item)
    False
    """
    fp = item.get_signature().fingerprint if '!' in pattern else None
    return match_key(pattern, item.key, fp)

def match_key(pattern, key, fp=None):
    """
    Test if the pattern matches the key.

    >>> match_key(['one', 'two'], ['one', 'two'])
    True
    >>> match_key(['one', 'two'], ['blah', 'two'])
    False
    >>> match_key(['one', 'two'], ['one', 'blah'])
    False
    >>> match_key(['one', 'two'], ['one', 'two', 'three'])
    False
    >>> match_key(['one', 'two', 'three'], ['one', 'two'])
    False

    >>> match_key(['one', '...'], ['one', 'two', 'three'])
    True
    >>> match_key(['one', '...'], ['one', 'two'])
    True
    >>> match_key(['one', '...'], ['one'])
    False

    >>> match_key(['one', '*', 'three'],  ['one', 'blah', 'three'], fp='1234')
    True
    >>> match_key(['*', '*', 'three'],  ['one', 'two', '*'], fp='1234')
    False
    >>> match_key(['one', '!', 'three'],  ['one', '1234', 'three'], fp='1234')
    True
    >>> match_key(['one', '!', 'three'],  ['one', 'blah', 'three'], fp='1234')
    False
    """
    assert pattern
    n = len(pattern)

    if pattern[-1] == '...':
        if len(key) < n:
            return False
        n -= 1
        pattern = pattern[:n]
        key = key[:n]
    else:
        if len(key) != n:
            return False

    for i in range(n):
        if pattern[i] == '*':
            pass
        elif pattern[i] == '!' and fp:
            if key[i] != fp:
                return False
        else:
            if key[i] != pattern[i]:
                return False

    return True

if __name__ == '__main__':
    import doctest
    doctest.testmod()
